分类
联系方式
  1. 新浪微博
  2. E-mail

BackTrader 三均线策略

介绍

三均线与《BackTrader SMA 金叉策略》类似,区别在于采用 3 条均线(短期、中期、长期)来作为决策点,将长期趋势也考虑在内。

本文策略是在《Backtrader来啦:常见案例汇总》一文中提出的,我进行了复现。

本文只用于数学、编程研究,不提供交易指导。

策略介绍

选取5日短期均线、20日中期均线,60日长期均线。

买入条件:当前无持仓,5日 > 20日 > 60日(多头排列),次日买入开仓。

卖出条件:当前有持仓,5日下穿20日,次日卖出清仓。

代码实现

代码实现也参照《Backtrader来啦:常见案例汇总》,从中学到一招,原来不用创建策略类,只靠自定义指标就能够直接参与交易。不过我还是把自定义指标加入我的策略类中来跑。

代码实现:

import backtrader as bt

from newstock.data.mongo.mongo_data_manager import MongoDataManager
from newstock.date.stock_date import StockDate
from newstock.market.Exchange import SZSEExchange
from newstock.market.symbol import Symbol
import pandas as pd


class MySignal(bt.Indicator):
    lines = ("signal",)  # 声明 signal 线,交易信号放在 signal line 上
    params = dict(short_period=5, median_period=20, long_period=60)

    def __init__(self):
        self.s_ma = bt.ind.SMA(period=self.p.short_period)
        self.m_ma = bt.ind.SMA(period=self.p.median_period)
        self.l_ma = bt.ind.SMA(period=self.p.long_period)
        # 短期均线在中期均线上方,且中期均取也在长期均线上方,三线多头排列,取值为1;反之,取值为0
        self.signal1 = bt.And(self.m_ma > self.l_ma, self.s_ma > self.m_ma)
        # 求上面 self.signal1 的环比增量,可以判断得到第一次同时满足上述条件的时间,第一次满足条件为1,其余条件为0
        self.buy_signal = bt.If((self.signal1 - self.signal1(-1)) > 0, 1, 0)
        # 短期均线下穿长期均线时,取值为1;反之取值为0
        self.sell_signal = bt.ind.CrossDown(self.s_ma, self.m_ma)
        # 将买卖信号合并成一个信号
        self.lines.signal = bt.Sum(self.buy_signal, self.sell_signal * (-1))


class TestStrategy(bt.Strategy):
    params = (("printlog", False),)

    def log(self, txt, dt=None, doprint=False):
        """Logging function for this strategy"""
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print("%s, %s" % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders
        self.order = None
        self.buyprice = None
        self.buycomm = None

        self.signal = MySignal(self.datas[0])
        self.s_ma = bt.ind.SMA(period=5)
        self.m_ma = bt.ind.SMA(period=20)
        self.l_ma = bt.ind.SMA(period=60)

        bt.indicators.MACDHisto(self.datas[0])

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # 交易完成
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

                self.buyprice = order.executed.price  # 买入价格
                self.buycomm = order.executed.comm  # 买入手续费
            elif order.issell():
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

            self.bar_executed = len(self)  # 买入日期

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        # Write down: no pending order
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log("Close, %.2f" % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # Not yet ... we MIGHT BUY if ...
            if self.signal == 1:
                # current close less than previous close

                # BUY, BUY, BUY!!! (with default parameters)
                self.log("BUY CREATE, %.2f" % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()

        else:

            # Already in the market ... we might sell
            if self.signal == -1:
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log("SELL CREATE, %.2f" % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

    def stop(self):
        self.log(
            "Ending Value %.2f" % (self.broker.getvalue()),
            doprint=True,
        )


if __name__ == "__main__":
    cerebro = bt.Cerebro()

    print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue())

    mongoManager = MongoDataManager()

    df = mongoManager.getStockPeriodFromDB(
        Symbol(SZSEExchange, "000001"),
        StockDate.today().previousDays(500),
        StockDate.today(),
    )
    df["date"] = pd.to_datetime(df["trade_date"], format="%Y%m%d")
    data = bt.feeds.PandasData(dataname=df, datetime="date")  # type: ignore
    df.dropna()

    # 0.1% ... divide by 100 to remove the %
    cerebro.broker.setcommission(commission=0.001)
    # Python 3.10 修复 module 'collections' has no attribute 'Iterable' 开始
    import collections

    collections.Iterable = collections.abc.Iterable
    # Python 3.10 修复 module 'collections' has no attribute 'Iterable' 完成
    # 策略参数优化
    # cerebro.optstrategy(TestStrategy, maperiod=range(10, 31))
    # 策略运行
    cerebro.addstrategy(TestStrategy)
    cerebro.adddata(data)
    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)
    cerebro.run()
    cerebro.plot(style="bar", volume=False)

    print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())

执行效果

原参数效果

由于我本地数据量并不够,因此没能完成一次有效交易。使用 zh000001 最近的交易日,效果如下图:

我发现,5日下穿20日作为一个卖出信号还是不错的

缩短周期

由于本地数据量不够,考虑缩短周期,用三均线作为短期策略使用,看看效果。

3条均线分别改为 5,15,20:

我发现,5,15,20 的多头排列作为开仓信号还不错

两者结合

既然:

  1. 5日下穿20日作为一个卖出信号还是不错的。
  2. 5,15,20 的多头排列作为开仓信号还不错。

两者结合一下会怎样呢?

效果跟想的一样,两个大的上涨波段都捕捉到了(第一个因为数据量不足,没有开仓成功)。

中间还试图捕捉一个小的趋势,但是失败了,并且损失还挺大。